/**
 * 
 */
package pacMan;

import java.awt.Graphics2D;
import java.util.ArrayList;

/**
 * 
 * Class: ObjectLocationTracker
 * Manages tracking physical location of objects on the game board. This is
 * represented by an M x N tiled grid, and we can have no more than one stationary
 * object occupying a tile at once. However objects can occupy more than one tile
 * at the same time.
 *
 * @author 		Tomas Rivera
 * @version 	$Id$
 * 
 */
public class ObjectLocationTracker {
	
	//////////////////////////////////////////////////////////
	// Enumeration definitions
	//////////////////////////////////////////////////////////
	
	public static enum SSOType { PELLET, ENERGIZER, EMPTY_LOCATION, TUNNEL_LOCATION }
	
	//////////////////////////////////////////////////////////
	// Constructor methods
	//////////////////////////////////////////////////////////
	
	/**
	 * Initializes BasicDrawableObject containers.
	 * 
	 * @param gridWidth
	 * @param gridHeight
	 * @param windowParams
	 */
	public ObjectLocationTracker(int gridWidth, int gridHeight, WindowParameters windowParams)
	{
		this.gridWidth = gridWidth;
		this.gridHeight = gridHeight;
		this.windowParams = windowParams;
		gridElements = new ArrayList<ArrayList<BasicDrawableObject>>(gridWidth);
		for(int i = 0; i < gridWidth; i++) {
			ArrayList<BasicDrawableObject> columns = new ArrayList<BasicDrawableObject>(gridHeight);
			gridElements.add(columns);
			for(int j = 0; j < gridHeight; j++) {
				columns.add(null);
			}
		}
		
		removedGridElements = new ArrayList<ArrayList<BasicDrawableObject>>(gridWidth);
		for(int i = 0; i < gridWidth; i++) {
			ArrayList<BasicDrawableObject> columns = new ArrayList<BasicDrawableObject>(gridHeight);
			removedGridElements.add(columns);
			for(int j = 0; j < gridHeight; j++) {
				columns.add(null);
			}
		}
		
		pellets = new Pellet(windowParams, ScoreTracker.PELLET_POINT_VALUE, this);
		energizers = new Energizer(windowParams, ScoreTracker.ENERGIZER_POINT_VALUE, this);
		emptyLocations = new EmptyPlaceHolder(windowParams, this, false);
		tunnelLocations = new EmptyPlaceHolder(windowParams, this, true);
		
		
		mazeElements = new ArrayList<BasicDrawableObject>();
		mazeElements.add(pellets);
		mazeElements.add(energizers);
		mazeElements.add(emptyLocations);
		mazeElements.add(tunnelLocations);
		
		bonusItem = new BonusItem(MazeBuilder.createCoordinateObj(13, 17, windowParams), windowParams);
		addStationaryObject(bonusItem, 13, 17);
		addStationaryObject(bonusItem, 14, 17);
		
		enemyActors = new ArrayList<Actor>();
	}
	
	//////////////////////////////////////////////////////////
	// Public methods
	//////////////////////////////////////////////////////////
	
	/**
	 * Helper function that handles issuing resizeObject() call
	 * to all BasicDrawableObjects managed by this container.
	 * 
	 */
	public void resizeStationaryMazeElements() {
		for(BasicDrawableObject element : mazeElements) {
			element.resizeObject();
		}
	}
	
	/**
	 * Helper function that handles issuing drawObject() call
	 * to all BasicDrawableObjects managed by this container.
	 * 
	 * @param g2d
	 */
	public void drawStationaryMazeElements(Graphics2D g2d) {
		for(BasicDrawableObject element : mazeElements) {
			element.drawObject(g2d);
		}
	}
	
	/**
	 * Draw the SuperpositionedStationaryObject at all of the locations where this tracking
	 * structure knows that the object is supposed to exist.
	 * 
	 * @param g2d
	 * @param wp
	 * @param obj
	 */
	public void drawStationaryObject(Graphics2D g2d, WindowParameters wp, SuperpositionedStationaryObject obj) {
		for(int i = 0; i < gridWidth; i++) {
			for(int j = 0; j < gridHeight; j++) {
				if(obj == getGridElement(i, j)) {
					obj.drawSuperpositionedObject(g2d, wp, i, j);
				}
			}
		}
	}
	
	
	
	/**
	 * Add SuperpositionedStationaryObject to the x,y grid location specified. This will result in the
	 * object being drawn at the center of that grid location.
	 * 
	 * @param ssoType
	 * @param x
	 * @param y
	 */
	public void addStationaryObject(SSOType ssoType, int x, int y) {
		switch(ssoType) {
			default:
				assert false;
			case PELLET:
				addStationaryObject(pellets, x, y);
				break;
			case ENERGIZER:
				addStationaryObject(energizers, x, y);
				break;
			case EMPTY_LOCATION:
				addStationaryObject(emptyLocations, x, y);
				break;
			case TUNNEL_LOCATION:
				addStationaryObject(tunnelLocations, x, y);
				break;
		}
	}
	
	/**
	 * Add BasicDrawableObject to the x,y grid location specified. This is for elements that
	 * that exist in a single tile or span multiple adjacent tiles.
	 * 
	 * @param obj
	 * @param x
	 * @param y
	 */
	public void addStationaryObject(BasicDrawableObject obj, int x, int y) {
		assert (null == getGridElement(x, y));
		 setGridElement(obj, x, y);
		 
		 if(!mazeElements.contains(obj)) {
			 mazeElements.add(obj);
		 }
		 
		//System.out.println(String.format("Added %s to tile x=%d, y=%d", obj.getClass().getName(), x, y));
	}
	
	/**
	 * Removes the object from the specified x,y grid location. For a SuperpositionedStationaryObject this
	 * will result in it no longer being drawn at that position on the screen but we save the original
	 * location so we can restore objects back later on. All other BasicDrawableObjects are just removed.
	 * 
	 * @param obj
	 * @param x
	 * @param y
	 */
	public void removeStationaryObject(SuperpositionedStationaryObject sso, int x, int y) {
		hideGridElement(x, y);
		sso.decrementCount();
		//System.out.println(String.format("Removed %s from tile x=%d, y=%d", sso.getClass().getName(), x, y));
	}
	
	/**
	 * Removes the object from the specified x,y grid location. For a SuperpositionedStationaryObject this
	 * will result in it no longer being drawn at that position on the screen but we save the original
	 * location so we can restore objects back later on. All other BasicDrawableObjects are just removed.
	 * 
	 * @param obj
	 * @param x
	 * @param y
	 */
	public void removeStationaryObject(BasicDrawableObject bdo, int x, int y) {
		setGridElement(null, x, y);
		//System.out.println(String.format("Removed %s from tile x=%d, y=%d", bdo.getClass().getName(), x, y));
	}
	
	/**
	 * Fills all spaces in the grid that have a null object with a reference to the 
	 * SuperpositionedStationaryObject that was passed. This is used as a shortcut
	 * to fill the maze with pellets without having to specify the placement
	 * for each one individually.
	 * 
	 * @param ssoType
	 */
	public void fillGrid(SSOType ssoType) {
		for(int i = 0; i < gridWidth; i++) {
			for(int j = 0; j < gridHeight; j++) {
				if(getGridElement(i, j) == null) {
					addStationaryObject(ssoType, i, j);
				}
			}
		}
	}

	/**
	 * Checks if the Actor object is allowed to move in the requested direction. Decision is 
	 * is based on the actor grid location and objects in surrounding grid locations.
	 * 
	 * @param actor
	 * @param direction
	 * @return
	 */
	public boolean legalMove(Actor actor, CardinalDirection direction) {
		
		// Check if object is outside the maze boundaries. If it is we only allow
		// movements EAST or WEST until we are back in-bounds. This handles the case
		// where Actor is within the maze tunnel.
		if(actor.getObjectLocation().getX() < (actor.getWindowParams().getPixelScale() * 2) ||
			actor.getObjectLocation().getX() > (actor.getWindowParams().getPixelScale() * (gridWidth - 1))) {
			switch(direction) {
				case NORTH:
				case SOUTH:
					return false;
				case EAST:
				case WEST:
					return true;
				default:
					break;
			}
		}
		
		switch(direction) {
			case NORTH:
				if(getGridElement(actor.getGridLocationX(), actor.getGridLocationY() - 1) != null &&
				   getGridElement(actor.getGridLocationX(), actor.getGridLocationY() - 1).isObstacle()) {
					if(actor.getObjectLocation().getY() % actor.getWindowParams().getPixelScale() == 0) {
						return false;
					}
				}
				break;
				
			case SOUTH:
				if(getGridElement(actor.getGridLocationX(), actor.getGridLocationY() + 1) != null &&
				   getGridElement(actor.getGridLocationX(), actor.getGridLocationY() + 1).isObstacle()) {
					return false;
				}
				break;
				
			case EAST:
				if(getGridElement(actor.getGridLocationX() + 1, actor.getGridLocationY()) != null &&
				   getGridElement(actor.getGridLocationX() + 1, actor.getGridLocationY()).isObstacle()) {
					return false;
				}
				break;
				
			case WEST:
				if(getGridElement(actor.getGridLocationX() - 1, actor.getGridLocationY()) != null &&
				   getGridElement(actor.getGridLocationX() - 1, actor.getGridLocationY()).isObstacle()) {
					if(actor.getObjectLocation().getX() % actor.getWindowParams().getPixelScale() == 0) {
						return false;
					}
				}
				break;
				
			default:
				break;
		}
		
		
		return true;
	}
	
	/**
	 * Checks if the Actor is currently on a tile that is part of a tunnel that connects
	 * opposite sides of the screen.
	 * 
	 * @param actor
	 * @return
	 */
	public boolean objectInTunnel(Actor actor) {
		if(objectInBounds(actor)) {
			BasicDrawableObject objAtLocation = getGridElement(actor.getGridLocationX(), actor.getGridLocationY());
		
			// A tunnel placeholder has the following properties:
			//		- Has single defined location (SuperpositionedStationaryObject)
			//		- Has no point value (EmptyPlaceHolder)
			//		- tunnelGridLocation flag is set
			return (objAtLocation != null && 
					objAtLocation.isTunnelPlaceHolder());
		}
		
		return true;
	}
	
	/**
	 * Checks if there is a consumable stationary object on the current tile, and
	 * calls consumeObject() if there one is found.
	 * 
	 * @param player
	 * @return
	 */
	public ConsumableObject checkForStationaryConsumable(Actor player) {
		// if consumable coordinate equals moveable object coordinate
		// then we consume the object.
		ConsumableObject consumable = null;
		
		if(objectInBounds(player) &&
			player.isCenteredHorizontally() &&
			player.isCenteredVertically() &&
			(consumable = (ConsumableObject) getGridElement(player.getGridLocationX(), player.getGridLocationY())) != null)
		{
			consumable.consumeObject(player);
		}
		
		return consumable;
	}
	
	/**
	 * Returns SuperpositionedStationaryObject back to the game board at all the locations
	 * where it was originally placed.
	 */
	public void restoreConsumedObjects(SuperpositionedStationaryObject sso) {
		assert (removedGridElements.size() == gridElements.size());
		
		for(int xGrid = 0; xGrid < removedGridElements.size(); xGrid++) {
			ArrayList<BasicDrawableObject> yLine = gridElements.get(xGrid);
			ArrayList<BasicDrawableObject> yLineRemoved = removedGridElements.get(xGrid);
			
			assert(yLine.size() == yLineRemoved.size());
			for(int yGrid = 0; yGrid < yLineRemoved.size(); yGrid++) {
				BasicDrawableObject obj = yLineRemoved.get(yGrid);
				if(sso.equals(obj)) {
					yLine.set(yGrid, sso);
					yLineRemoved.set(yGrid, null);
					sso.incrementCount();
				}
			}
		}
	}
	
	/**
	 * Save a reference to the player Actor locally.
	 * 
	 * @param player
	 */
	public void registerPlayerObject(Actor player) { this.player = player; }
	
	/**
	 * Save a reference to the enemy Actor locally.
	 * 
	 * @param enemyActor
	 */
	public void registerEnemyActor(Actor enemyActor) { enemyActors.add(enemyActor); }
	
	/**
	 * Get the player Actor Coordinate object.
	 * 
	 * @return
	 */
	public Coordinate getPlayerLocation() { return player.getObjectLocation(); }
	
	/**
	 * Get the player Actor CardinalDirection.
	 * 
	 * @return
	 */
	public CardinalDirection getPlayerDirection() { return player.getDirection(); }
	
	/**
	 * We define an actor collision as when two of them are occupying the same "tile". If the ghost
	 * is in scared mode, it can then be consumed by the player. Otherwise this is registered as
	 * a "kill" by the ghost.
	 * 
	 * @param enemyActor
	 * @return
	 */
	public boolean checkForEnemyPlayerCollision(Actor enemyActor) {
		if(player.getGridLocationX() == enemyActor.getGridLocationX() &&
			player.getGridLocationY() == enemyActor.getGridLocationY()) 
		{
			player.consumeObject(enemyActor);
			return true;
		}
		
		return false;
	}
	
	/**
	 * We define an actor collision as when two of them are occupying the same "tile". If the ghost
	 * is frightened, it can then be consumed by the player. Otherwise this is registered as
	 * a "kill" by the ghost.
	 * 
	 * @return Actor
	 */
	public Actor checkForPlayerEnemyCollision() {
		for(Actor enemyActor : enemyActors) {
			if(player.getGridLocationX() == enemyActor.getGridLocationX() &&
				player.getGridLocationY() == enemyActor.getGridLocationY()) {
				return enemyActor;
			}
		}
		
		return null;
	}
	
	/**
	 * Get the tile grid row location where the Coordinate would exist on the game board.
	 * 
	 * @param objectLocation
	 * @return
	 */
	public int getGridLocationX(Coordinate objectLocation) {
		return (objectLocation.getX() - (objectLocation.getX() % windowParams.getPixelScale()) - windowParams.getPixelOffsetX()) / windowParams.getPixelScale();
	}
	
	/**
	 * Get the tile grid column location where the Coordinate would exist on the game board.
	 * 
	 * @param objectLocation
	 * @return
	 */
	public int getGridLocationY(Coordinate objectLocation) {
		return (objectLocation.getY() - (objectLocation.getY() % windowParams.getPixelScale()) - windowParams.getPixelOffsetY()) / windowParams.getPixelScale();
	}
	
	/**
	 * Checks if the enemy Actor is allowed to move North given their current location. This checks
	 * for special cases where we disallow the movement algorithm to direct the Actor from
	 * following certain paths.
	 * 
	 * @param enemyActor
	 * @return
	 */
	public boolean isEnemyUpwardMovementAllowed(Actor enemyActor) {
		// TODO: Blocking movement of ghosts in certain directions when at specific
		// locations should be something setup in the txt config file. Leaving this
		// as is for now...
		if(enemyActor.getGridLocationX() > 10 && enemyActor.getGridLocationX() < 17 &&
		   (enemyActor.getGridLocationY() == 11 || enemyActor.getGridLocationY() == 23))
		{
			return false;
		}
		
		return true;
	}
	
	/**
	 * Get the current count of pellets and energizers that have not yet been consumed.
	 * 
	 * @return
	 */
	public int getPelletCount() { return pellets.getVisibleCount() + energizers.getVisibleCount(); }
	
	/**
	 * Get the total number of pellets and energizers that were originally generated by
	 * the MazeBuilder.
	 * 
	 * @return
	 */
	public int getTotalGeneratedCount() { return pellets.getGeneratedCount() + energizers.getGeneratedCount(); }
	
	/**
	 * Replace all pellets that were consumed by the player. Used to reset the game board
	 * for a new game or new round.
	 * 
	 */
	public void restoreStationaryConsumables() {
		pellets.reset();
		energizers.reset();
	}
	
	//////////////////////////////////////////////////////////
	// Private methods
	//////////////////////////////////////////////////////////
	
	/*
	 * This is intended to be used as a quick check to see if the Actor is outside
	 * of the game board boundaries as they were originally defined.
	 * 
	 */
	private boolean objectInBounds(Actor actor) {
		int gridX = actor.getGridLocationX();
		int gridY = actor.getGridLocationY();
		
		// Legal tiles are from 1 to (gridWidth - 1)
		// Anything outside that range is either a wall or off
		// the game board.
		if(gridX > 0 && gridX < (gridWidth - 1) &&
			gridY > 0 && gridY < (gridHeight - 1)) {
			return true;
		}
		
		return false;
	}
	
	/*
	 * Add SuperpositionedStationaryObject to the x,y grid location specified. This will result in the
	 * object being drawn at the center of that grid location.
	 * 
	 */
	private void addStationaryObject(SuperpositionedStationaryObject obj, int x, int y) {
		assert (null == getGridElement(x, y));
		setGridElement(obj, x, y);
		//System.out.println(String.format("Added %s to tile x=%d, y=%d", obj.getClass().getName(), x, y));
		obj.incrementCount();
	}
	
	/*
	 * Get the object that is referenced at the X,Y grid location specified.
	 * 
	 */
	private BasicDrawableObject getGridElement(int xGrid, int yGrid) {
		if(xGrid < gridElements.size()) {
			ArrayList<BasicDrawableObject> yLine = gridElements.get(xGrid);
			if(yGrid < yLine.size()) {
				return yLine.get(yGrid);
			}
		}
		
		return null;
	}
	
	/*
	 * Save a reference to the BasicDrawableObject at the X,Y grid location specified.
	 */
	private void setGridElement(BasicDrawableObject bdo, int xGrid, int yGrid) {
		if(xGrid < gridElements.size()) {
			ArrayList<BasicDrawableObject> yLine = gridElements.get(xGrid);
			if(yGrid < yLine.size()) {
				yLine.set(yGrid, bdo);
				return;
			}
		}
		
		assert false;
	}
	
	/*
	 * Remove reference to BasicDrawableObject from the X,Y grid location specified and save
	 * in backup location.
	 */
	private void hideGridElement(int xGrid, int yGrid) {
		// TODO: this should check if this is a removable object. Shouldn't allow
		// for removing a Wall for instance...
		if(xGrid < gridElements.size() && xGrid < removedGridElements.size()) {
			ArrayList<BasicDrawableObject> yLine = gridElements.get(xGrid);
			ArrayList<BasicDrawableObject> yLineRemove = removedGridElements.get(xGrid); 
			if(yGrid < yLine.size() && yGrid < yLineRemove.size() && yLine.get(yGrid) != null) {
				yLineRemove.set(yGrid, yLine.get(yGrid));
				yLine.set(yGrid, null);
				return;
			}
		}
		
		assert false;
	}
	
	//////////////////////////////////////////////////////////
	// Private member variables
	//////////////////////////////////////////////////////////
	
	private WindowParameters windowParams;
	private Pellet pellets;
	private Energizer energizers;
	private EmptyPlaceHolder emptyLocations;
	private EmptyPlaceHolder tunnelLocations;
	private BonusItem bonusItem;
	private ArrayList<BasicDrawableObject> mazeElements;
	private ArrayList<ArrayList<BasicDrawableObject>> gridElements;
	private ArrayList<ArrayList<BasicDrawableObject>> removedGridElements;
	private Actor player;
	private ArrayList<Actor> enemyActors;
	private int gridWidth;
	private int gridHeight;

}
